﻿using System;
using System.Collections.Generic;
using System.Text;
using SocialExplorer.IO.FastDBF;
using System.IO;

namespace Soil
{
    class Soil
    {
        private string _name;
        private string _name_short;
        private string _id;
        private int _rootRestrictLayerId = -9;
        private string _drainage;
        private double _anion_excl = 0.5; //SWAT default value
        private double _soil_crk = 0.0; //no crack by default
        private HydrologicalGroupType _hydrologicalGroup = HydrologicalGroupType.UNKNOWN;
        private double _sol_zmx = -1;

        private string _province;
        private string _soil_code;
        private string _modifer;
        private string _profile;
        private string _id_code;
        private string _id_modifer;

        private SortedDictionary<int,SoilLayer> _layers = new SortedDictionary<int,SoilLayer>();

        public SortedDictionary<int, SoilLayer> Layers { get { return _layers; } }

        public SoilLayer GetLayer(int layerNo)
        {
            if (_layers.ContainsKey(layerNo)) return _layers[layerNo];
            return null;
        }

        public string ID { get { return _id; } }
        public string ID_CODE { get { return _id_code; } }
        public string ID_MODIFER { get { return _id_modifer; } }
        public string Name { get { return _name; } }
        public string Name_Short { get { return _name_short; } }
        private int _lookup_index = -1;
        public int LookupIndex { get { return _lookup_index; } set { _lookup_index = value; } }

        /// <summary>
        /// Modify soil name to avoid same name for differnt soil type
        /// </summary>
        /// <param name="modifier">Should be between 1 and 99 to make sure the length of name string is not more than 25</param>
        /// <remarks>Just add number start from 1</remarks>
        public void ModifyName(int modifier)
        {
            _name_short += modifier.ToString();
        }

        public void AddLayer(SoilLayer layer)
        {
            if (!_layers.ContainsKey(layer.Layer))
                _layers.Add(layer.Layer, layer);
        }

        public void AddLayer(List<SoilLayer> layers)
        {
            foreach (SoilLayer layer in layers)
                AddLayer(layer);

            //SWAT only could handle max 10 soil layers
            //if there are two many layers, merge some layers to make up to 10 layers
            if (_layers.Count > 10) 
            {
                SoilLayer lastLayer = null;
                for (int i = 10; i <= _layers.Count; i++)
                {
                    if (lastLayer == null) lastLayer = _layers[i];
                    else lastLayer = lastLayer.Connect(_layers[i]);
                }
                lastLayer.Layer = 10;

                while (_layers.Count >= 10)
                    _layers.Remove(_layers.Count);
                _layers.Add(10, lastLayer);
            }
        }

        public int NLAYERS { get { return _layers.Count; } }

        /// <summary>
        /// Yongbo method: Determin hydrological group from hydraulic conductivity of the first layer
        /// </summary>
        public HydrologicalGroupType HYDGRP_Yongbo
        {
            get
            {
                if(_hydrologicalGroup == HydrologicalGroupType.UNKNOWN)
                {
                    _hydrologicalGroup = HydrologicalGroupType.A;
                    if (_layers.Count > 0)
                    {
                        double k = _layers[1].SOL_K;

                        if (k >= 7.6 && k <= 11.4) _hydrologicalGroup =  HydrologicalGroupType.A;
                        if (k >= 3.8 && k <= 7.6) _hydrologicalGroup =  HydrologicalGroupType.B;
                        if (k >= 1.3 && k <= 3.8) _hydrologicalGroup = HydrologicalGroupType.C;
                        if (k >= 0 && k <= 1.3) _hydrologicalGroup = HydrologicalGroupType.D;                        
                    }
                }
                return _hydrologicalGroup;
            }
        }

        /// <summary>
        /// EC method: Determine hydrological group from column drainage
        /// </summary>
        public HydrologicalGroupType HYDGRP_EC
        {
            get
            {
                if(_hydrologicalGroup == HydrologicalGroupType.UNKNOWN)
                {
                    _hydrologicalGroup = HydrologicalGroupType.A;
                    if (_drainage.Equals("VR") || _drainage.Equals("R")) _hydrologicalGroup =   HydrologicalGroupType.A;
                    if (_drainage.Equals("MW") || _drainage.Equals("W")) _hydrologicalGroup =   HydrologicalGroupType.B;
                    if (_drainage.Equals("I")) _hydrologicalGroup =   HydrologicalGroupType.C;
                    if (_drainage.Equals("P") || _drainage.Equals("VP")) _hydrologicalGroup =   HydrologicalGroupType.D;
                }
                return _hydrologicalGroup;
            }
        }

        public double SOL_ZMX
        {
            get
            {
                if(_sol_zmx < 0)
                {
                    if (_rootRestrictLayerId > 0 && _rootRestrictLayerId <= _layers.Count)
                        _sol_zmx = _layers[_rootRestrictLayerId].SOL_Z;
                    else if (_layers.Count > 0)
                        _sol_zmx =  _layers[_layers.Count].SOL_Z;
                    else
                        _sol_zmx =  SoilLayer.SWAT_EMPTY_VALUE;
                }
                return _sol_zmx;
            }
        }

        /// <summary>
        /// To make all depth large than 0
        /// 
        /// For mineral soils the top of the mineral material is chosen as depth zero, 
        /// and for any organic layers that are present (such as LFH) the values of both UDEPTH and LDEPTH 
        /// will be negative.
        /// </summary>
        public void AdjustNegativeDepth()
        {
            if (_layers.Count == 0) return;
            if (_layers[1].SOL_TOP_Z >= 0) return;
            double adjust = Math.Abs(_layers[1].SOL_TOP_Z);
            foreach (SoilLayer layer in _layers.Values)
                layer.LowerDepth(adjust);
        }

        /// <summary>
        /// AWC is zero for some layers. Get average AWC for these layers
        /// </summary>
        public void AdjustZeroAWC()
        {
            if (_layers.Count == 0) return;

            bool hasZero = false;
            foreach(SoilLayer layer in _layers.Values)
                if(layer.SOL_AWC == 0)
                {
                    hasZero = true;
                    break;
                }

            if(!hasZero) return;

            double ave = 0.0;
            int num = 0;
            for(int i=1;i<=NLAYERS;i++)
                if (_layers[i].SOL_AWC > 0)
                {
                    ave += _layers[i].SOL_AWC;
                    num ++;
                }

            if (num == 0) return; //all zero

            ave /= num;
            foreach (SoilLayer layer in _layers.Values)
                if (layer.SOL_AWC == 0) layer.SOL_AWC = ave;
        }

        private bool _isSoil = true;

        public bool IsSoil { get { return _isSoil; } }

        public Soil(DbfRecord soilRecord)
        {
            _id = soilRecord["SOIL_ID"];
            _name = soilRecord["SOILNAME"].Trim();
            if (_name.IndexOf(',') >= 0)
                _name = _name.Substring(0, _name.IndexOf(',')).Trim(); //remove , to avoid output error in csv format
            _name_short = _name.Length > 23 ? _name.Substring(0, 23).ToUpper() : _name.ToUpper();

            int.TryParse(soilRecord["ROOTRESTRI"], out _rootRestrictLayerId);
            _drainage = soilRecord["DRAINAGE"];
            
            string kind = soilRecord["KIND"];
            if (kind.Equals("M") || kind.Equals("O"))
                _isSoil = true;
            else
                _isSoil = false;

            _province = soilRecord["PROVINCE"];
            _soil_code = soilRecord["SOIL_CODE"];
            _modifer = soilRecord["MODIFIER"];
            _profile = soilRecord["PROFILE"];
            _id_code = _province + _soil_code;
            _id_modifer = _id_code + _modifer;
        }

        public Soil(string id, string name, HydrologicalGroupType group, double sol_zmx,bool isSoil)
        {
            _id = id;
            _name = name;
            _name_short = _name.Length > 23 ? _name.Substring(0, 23).ToUpper() : _name.ToUpper();
            _hydrologicalGroup = group;
            _sol_zmx = sol_zmx;
            _isSoil = isSoil;
        }

        public static string SWATHeader
        {
            get
            {
                string profile = "MUID,SEQN,SNAM,S5ID,CMPPCT,NLAYERS,HYDGRP,SOL_ZMX,ANION_EXCL,SOL_CRK,TEXTURE";
                for (int i = 1; i <= 10; i++)
                    profile += "," + string.Format("SOL_Z{0},SOL_BD{0},SOL_AWC{0},SOL_K{0},SOL_CBN{0},CLAY{0},SILT{0},SAND{0},ROCK{0},SOL_ALB{0},USLE_K{0},SOL_EC{0}", i);
                for (int i = 1; i <= 10; i++)
                    profile += "," + string.Format("SOL_CAL{0}", i);
                for (int i = 1; i <= 10; i++)
                    profile += "," + string.Format("SOL_PH{0}", i);

                return profile;
            }
        }

        public override string ToString()
        {
            string profile = string.Format("zhiqiang,,{0},zhiqiang,,{1},{2},{3},{4},{5},zhiqiang",
                Name_Short,
                NLAYERS, HYDGRP_Yongbo, SOL_ZMX, _anion_excl,_soil_crk); //the soil name could be very long so just use the first 25 letters
            for (int i = 1; i <= 10; i++)
            {
                if (i <= _layers.Count)
                    profile += "," + _layers[i].ToString();
                else
                    profile += "," + SoilLayer.EMPTY_VALUES;
            }
            for (int i = 1; i <= 10; i++)
            {
                if (i <= _layers.Count)
                    profile += "," + _layers[i].SOL_CAL.ToString();
                else
                    profile += ",0.0";
            }
            for (int i = 1; i <= 10; i++)
            {
                if (i <= _layers.Count)
                    profile += "," + _layers[i].SOL_PH.ToString();
                else
                    profile += ",0.0";
            }

            return profile;
        }

        /// <summary>
        /// Get soil layer between given depth, new soil layer would be created if necessary and parameter would be re-calculated
        /// </summary>
        /// <param name="top_depth"></param>
        /// <param name="bottom_depth"></param>
        /// <returns></returns>
        public SoilLayer GetLayer(double top_depth, double bottom_depth)
        {
            SoilLayer returnlayer = null;
            foreach (SoilLayer shortLayer in Layers.Values)
            {
                SoilLayer childShortLayer = shortLayer.Clone(top_depth, bottom_depth);
                if (childShortLayer == null) continue;

                if (returnlayer == null) returnlayer = childShortLayer;
                else returnlayer = returnlayer.Connect(childShortLayer);
            }
            return returnlayer;
        }

        /// <summary>
        /// Merge with other soil to get a new soil
        /// </summary>
        /// <param name="comp"></param>
        /// <returns></returns>
        public Soil Merge(Soil other, double percentage, double percentage_other)
        {
            if (other == null) return this;
            if (ID.Equals(other.ID)) return this;
            if (other.NLAYERS == 0) return this;
            if (NLAYERS == 0) return other;            
            if (!other.IsSoil) return this;
            if (!this.IsSoil) return other;

            System.Diagnostics.Debug.WriteLine(string.Format("Merging Soils: {0}({1}), {2}({3})",
                ID,Name,other.ID,other.Name));

            //get all depth
            List<double> allDepths = new List<double>();
            foreach (SoilLayer layer in Layers.Values)
                allDepths.Add(layer.SOL_Z);
            foreach (SoilLayer layer in other.Layers.Values)
                if(!allDepths.Contains(layer.SOL_Z))
                    allDepths.Add(layer.SOL_Z);

            //sort ase
            allDepths.Sort();

            //merge all layers
            List<SoilLayer> newMergedLayers = new List<SoilLayer>();
            for(int i=0;i<allDepths.Count;i++)
            {
                double top = 0.0;
                if(i>0) top = allDepths[i-1];
                double bottom = allDepths[i];
                if (bottom <= top) continue;

                SoilLayer layer1 = this.GetLayer(top, bottom);
                SoilLayer layer2 = other.GetLayer(top, bottom);
                if (layer1 == null && layer2 == null)
                    throw new Exception("Depth Error?");

                SoilLayer newLayer = null;
                if (layer1 == null && layer2 != null) newLayer = layer2;
                if (layer2 == null && layer1 != null) newLayer = layer1;
                if (layer1 != null && layer2 != null) newLayer = layer1.Merge(percentage, layer2, percentage_other);

                if (newLayer.Layer != i + 1)
                    newLayer.Layer = i + 1;

                newMergedLayers.Add(newLayer);
            }

            //creat new merged soil
            string newSoilID = string.Format("({0}){1}-({2}){3}",
                ID, percentage, other.ID, percentage_other);
            string newSoilName = Name;
            if (!Name.Contains(other.Name)) 
                newSoilName += "-" + other.Name;

            Soil newSoil = new Soil(newSoilID, newSoilName, 
                (HydrologicalGroupType)(Math.Max((int)(HYDGRP_Yongbo),(int)(other.HYDGRP_Yongbo))), 
                Math.Max(SOL_ZMX,other.SOL_ZMX),true);
            newSoil.AddLayer(newMergedLayers);
            return newSoil;
        }
    }
}
